/*****************************************************************************
 *  Include Section
 *  add all #include here
 *****************************************************************************/
#include "fh_mshc_lite.h"
#include "fh_mshc_lite_dll.h"
/*****************************************************************************
 * Define section
 * add all #define here
 *****************************************************************************/
#define MMC_BUS_WIDTH_1		0
#define MMC_BUS_WIDTH_4		2
#define MMC_BUS_WIDTH_8		3
/****************************************************************************
 * ADT section
 *  add definition of user defined Data Type that only be used in this file  here
 ***************************************************************************/


/*****************************************************************************
 *  static fun;
 *****************************************************************************/


/*****************************************************************************
 *  extern fun;
 *****************************************************************************/


/*****************************************************************************
 * Global variables section - Local
 * define global variables(will be refered only in this file) here,
 * static keyword should be used to limit scope of local variable to this file
 * e.g.
 *  static uint8_t ufoo;
 *****************************************************************************/
u8 *mmc_width_str[] = {
	"MMC_BUS_WIDTH_1",
	"UNKNOW",
	"MMC_BUS_WIDTH_4",
	"MMC_BUS_WIDTH_8",
};


u8 *mmc_timing_str[] = {
	"MMC_TIMING_LEGACY",
	"MMC_TIMING_MMC_HS",
	"MMC_TIMING_SD_HS",
	"MMC_TIMING_UHS_SDR12",
	"MMC_TIMING_UHS_SDR25",
	"MMC_TIMING_UHS_SDR50",
	"MMC_TIMING_UHS_SDR104",
	"MMC_TIMING_UHS_DDR50",
	"MMC_TIMING_MMC_DDR52",
	"MMC_TIMING_MMC_HS200",
	"MMC_TIMING_MMC_HS400",
};

#if(0)
//parse cmdline buf
struct mshc_lite_emmc_card_info g_emmc_info = {.mmc_name = "NULL"};
char g_name_array[256] = {0};
#endif
/*****************************************************************************
 * func below..
 *****************************************************************************/
void fh_mshc_gotid_callback(struct sdhci_host *host, u32 *cid){

}


void sdhci_do_enable_v4_mode(struct sdhci_host *host)
{
	u16 ctrl2;

	ctrl2 = sdhci_readw(host, SDHCI_HOST_CONTROL2);
	if (ctrl2 & 0x1000)
		return;
	ctrl2 |= 0x1000;
	sdhci_writew(host, ctrl2, SDHCI_HOST_CONTROL2);
}


void sdhci_do_enable_es_mode(struct sdhci_host *host, u32 enable)
{
	u32 ctrl;
	ctrl = sdhci_readl(host, EMMC_CTRL_R);
	if (enable)
		ctrl |= SDHCI_ENH_STROBE_EN;
	else
		ctrl &= ~SDHCI_ENH_STROBE_EN;

	sdhci_writel(host, ctrl, EMMC_CTRL_R);
}


void fh_mshc_ctrl_reset(struct sdhci_host *host, u8 mask)
{

	unsigned long timeout;
	/* Wait max 100 ms */
	timeout = 100;
	sdhci_writeb(host, mask, SDHCI_SOFTWARE_RESET);
	while (sdhci_readb(host, SDHCI_SOFTWARE_RESET) & mask) {
		if (timeout == 0) {
			pr_err("%s: Reset 0x%x never completed.\n",
			       __func__, (int)mask);
			return;
		}
		timeout--;
		udelay(1000);
	}

}

void fh_mshc_tuning_ctrl_init(struct sdhci_host *host){

	u32 isr_set = SDHCI_INT_DATA_MASK | SDHCI_INT_CMD_MASK;

	sdhci_reset(host, SDHCI_RESET_ALL);
	//tbd
#if(0)
	if(host->flags & SDHCI_AUTO_CMD12)
		isr_set |= SDHCI_INT_ACMD12ERR;
#endif
	/* Enable only interrupts served by the SD controller */
	sdhci_writel(host, isr_set, SDHCI_INT_ENABLE);
	/* Mask all sdhci interrupt sources */
	sdhci_writel(host, 0x0, SDHCI_SIGNAL_ENABLE);
	sdhci_writew(host, SDHCI_CARD_IS_EMMC, EMMC_CTRL_R);

	//sdhci_do_enable_v4_mode(host);
}

void fh_mshc_ctrl_init_for_kernel(struct sdhci_host *host, u32 es_sup){

	fh_mshc_ctrl_reset(host, SDHCI_RESET_ALL);
	sdhci_writel(host, host->ier, SDHCI_INT_ENABLE);
	sdhci_writel(host, host->ier, SDHCI_SIGNAL_ENABLE);
	sdhci_writew(host, 0x1 | (es_sup << 8), EMMC_CTRL_R);
	//sdhci_do_enable_v4_mode(host);
}


void fh_mshc_tuning_ctrl_set_width_and_clkmode(struct sdhci_host *host,  u32 clk, u32 timing, u32 width)
{
	u32 ctrl;
	u16 ctrl_2;
	/* Set bus width */
	ctrl = sdhci_readb(host, SDHCI_HOST_CONTROL);
	if (width == MMC_BUS_WIDTH_8) {
		ctrl &= ~SDHCI_CTRL_4BITBUS;
		ctrl |= SDHCI_CTRL_8BITBUS;
	} else {
		ctrl &= ~SDHCI_CTRL_8BITBUS;
		if (width == MMC_BUS_WIDTH_4)
			ctrl |= SDHCI_CTRL_4BITBUS;
		else
			ctrl &= ~SDHCI_CTRL_4BITBUS;
	}

	if (clk > 26000000)
		ctrl |= SDHCI_CTRL_HISPD;
	else
		ctrl &= ~SDHCI_CTRL_HISPD;


	sdhci_writeb(host, ctrl, SDHCI_HOST_CONTROL);

	/*set timing*/
	ctrl_2 = sdhci_readw(host, SDHCI_HOST_CONTROL2);
	/* Select Bus Speed Mode for host */
	ctrl_2 &= ~SDHCI_CTRL_UHS_MASK;

	if(timing == MMC_TIMING_MMC_HS200)
		ctrl_2 |= 3;
	else if(timing == MMC_TIMING_MMC_HS400)
		ctrl_2 |= 7;
	else
		ctrl_2 |= 1;

	sdhci_writew(host, ctrl_2, SDHCI_HOST_CONTROL2);
}


void fh_mshc_tuning_set_clock(struct sdhci_host *host, u32 clock, u32 timing, u32 width, u32 es_mode)
{

	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);

	if(p_mshc->mshc_plat_info->clk_set)
		p_mshc->mshc_plat_info->clk_set(host, clock);
	phy_clear_rxfifo(p_mshc);
	//pr_err("%s :: clock : timing : width == %d : %x : %x\n",__func__, clock , timing , width);
	//pr_err("es_mode is %d\n",es_mode);
	fh_mshc_tuning_ctrl_set_width_and_clkmode(host,  clock, timing, width);

	if(timing == MMC_TIMING_MMC_HS400 && es_mode == EMMC_ES_MODE_ENABLE){
		sdhci_do_enable_es_mode(host, 1);
	}
	else{
		sdhci_do_enable_es_mode(host, 0);
	}

	if(timing == MMC_TIMING_MMC_HS400){
		phy_tx_dri_clk_sel(p_mshc, PHY_TX_DRI_SEL_CLK_EMMC_2X);
		if(es_mode == EMMC_ES_MODE_ENABLE)
			phy_rx_cmd_sample_clk_sel(p_mshc, PHY_RX_CMD_SEL_CLK_DS);
		else
			phy_rx_cmd_sample_clk_sel(p_mshc, PHY_RX_CMD_SEL_CLK_EMMC);
		phy_rx_data_sample_clk_sel(p_mshc, PHY_RX_DATA_SEL_CLK_DS);
	}
	else{
		phy_tx_dri_clk_sel(p_mshc, PHY_TX_DRI_SEL_CLK_EMMC);
		phy_rx_data_sample_clk_sel(p_mshc, PHY_RX_DATA_SEL_CLK_EMMC);
		phy_rx_cmd_sample_clk_sel(p_mshc, PHY_RX_CMD_SEL_CLK_EMMC);
	}
}



void fh_mshc_hs_setting(struct sdhci_host *host, u32 clock, u32 width,
u32 es_mode, u32 timing, struct fh_mshc_phy_dll *p_dll)
{
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);

	//first set dll
	phy_dly_setting(p_mshc, p_dll->tx_dly, p_dll->rxdata_dly,p_dll->rxcmd_dly);
	//then set clk..
	fh_mshc_tuning_set_clock(host, clock, timing, width, es_mode);
	//no clear fifo
}

void fh_mshc_hs200_setting(struct sdhci_host *host, u32 clock, u32 width,
u32 es_mode, u32 timing, struct fh_mshc_phy_dll *p_dll)
{
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);

	//first set clk
	fh_mshc_tuning_set_clock(host, clock, timing, width, es_mode);
	//then set dll
	phy_dly_setting(p_mshc, p_dll->tx_dly, p_dll->rxdata_dly,p_dll->rxcmd_dly);
	phy_clear_rxfifo(p_mshc);
}

void fh_mshc_hs400_setting(struct sdhci_host *host, u32 clock, u32 width,
u32 es_mode, u32 timing, struct fh_mshc_phy_dll *p_dll)
{
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);

	//first set dll
	phy_dly_setting(p_mshc, p_dll->tx_dly, p_dll->rxdata_dly,p_dll->rxcmd_dly);
	//then set clk..
	fh_mshc_tuning_set_clock(host, clock, timing, width, es_mode);
	//no clear fifo
}


void fh_mshc_set_clock(struct sdhci_host *host, unsigned int clock)
{
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);
	struct mshc_lite_emmc_card_info *p_info;
	u32 timing;
	u32 width;
	u32 es_mode;
	struct fh_mshc_phy_dll dll = {0};

	if (clock == 0)
		return;
	p_info = p_mshc->bind_active_tuning_emmc;
	timing = p_mshc->rec_timing;
	width = p_mshc->rec_bus_width;
	es_mode = p_mshc->bind_mmc->ios.enhanced_strobe;

	//pr_err("clock  :: [%d]\nwidth  :: [%s]\ntiming :: [%s]\n", clock, mmc_width_str[width], mmc_timing_str[timing]);
	//pr_err("host cap2 is %x\n",p_mshc->bind_mmc->caps2);
	//pr_err("p_mshc->bind_mmc->ios->enhanced_strobe is %x\n",p_mshc->bind_mmc->ios.enhanced_strobe);
	if(p_mshc->mshc_plat_info->soc_ctrl_reset)
		p_mshc->mshc_plat_info->soc_ctrl_reset(host);

	mshc_phy_init(p_mshc, INIT_INV_DISABLE);
	fh_mshc_ctrl_init_for_kernel(host, es_mode);


	if (timing == MMC_TIMING_MMC_HS400) {
		dll.tx_dly = p_info->hs400_tx_data;
		dll.rxdata_dly = p_info->hs400_rx_data;
		dll.rxcmd_dly = p_info->hs400_rx_cmd;
		fh_mshc_hs400_setting(host, clock, width, es_mode, timing, &dll);
	}
	else{
		if (clock > 52000000) {
			dll.tx_dly = p_info->hs200_tx_data;
			dll.rxdata_dly = p_info->hs200_rx_data;
			dll.rxcmd_dly = p_info->hs200_rx_cmd;
			fh_mshc_hs200_setting(host, clock, width, es_mode, timing, &dll);
		}
		else {
			dll.tx_dly = p_info->hs_tx_data;
			dll.rxdata_dly = p_info->hs_rx_data;
			dll.rxcmd_dly = p_info->hs_rx_cmd;
			fh_mshc_hs_setting(host, clock, width, es_mode, timing, &dll);
		}
	}
}

void fh_mshc_set_power(struct sdhci_host *host, unsigned char mode,
unsigned short vdd){

	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);
	p_mshc->rec_power = vdd;

}

void fh_mshc_set_bus_width(struct sdhci_host *host, int width){
	//null
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);
	p_mshc->rec_bus_width = width;
}


void fh_mshc_set_uhs_signaling(struct sdhci_host *host, unsigned int uhs){
	//null
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
	struct fh_mshc_lite *p_mshc = sdhci_pltfm_priv(pltfm_host);
	p_mshc->rec_timing = uhs;

}

static const struct sdhci_ops sdhci_mc_ops = {
	.reset = sdhci_reset,
	.set_power = fh_mshc_set_power,
	.set_clock = fh_mshc_set_clock,
	.set_bus_width = fh_mshc_set_bus_width,
	.set_uhs_signaling = fh_mshc_set_uhs_signaling,
	.get_max_clock		= sdhci_pltfm_clk_get_max_clock,
};

static const struct sdhci_pltfm_data sdhci_mc_pdata = {
	.quirks = SDHCI_QUIRK_BROKEN_CARD_DETECTION |
		  SDHCI_QUIRK_NO_CARD_NO_RESET |
		  SDHCI_QUIRK_SINGLE_POWER_WRITE | SDHCI_QUIRK_RESET_CMD_DATA_ON_IOS ,
	.ops = &sdhci_mc_ops,
};


int bind_emmc_card_info(struct fh_mshc_lite *fh_mshc, struct mshc_lite_plat_info *plat_info)
{
	u8 emmc_name[16] = {0};
	u32 ret_cid[4] = {0};
	int ret;
	struct mshc_lite_emmc_card_info *emmc_pinfo = 0;

	ret = mshc_auto_get_mmc_cid(fh_mshc, ret_cid, emmc_name);
	if (ret){
		pr_err("%s :: err is %x\n", __func__, ret);
		return ret;
	}
	emmc_pinfo = emmc_name_to_plat_info(plat_info->emmc_info_list, emmc_name);
	if (!emmc_pinfo){
		pr_err("pls add board emmc[%s] info..assert\n",emmc_name);
		BUG_ON(1);
	}
	//here memcpy name to mmc name buf.
	strcpy(emmc_pinfo->mmc_name_buf, emmc_pinfo->mmc_name);
	pr_err("emmc_pinfo->mmc_name_buf is %s...\n",emmc_pinfo->mmc_name_buf);
	fh_mshc->bind_active_tuning_emmc = emmc_pinfo;
	return 0;
}


void translate_auto_to_kernel(struct fh_mshc_lite *fh_mshc, struct mshc_lite_emmc_card_info *emmc_info){
	struct sdhci_host *host;

	host = fh_mshc->bind_host;
	host->mmc->caps = (MMC_CAP_NONREMOVABLE | MMC_CAP_8_BIT_DATA | MMC_CAP_1_8V_DDR | MMC_CAP_MMC_HIGHSPEED);
	host->mmc->ocr_avail = MMC_VDD_165_195;
	fh_mshc->bind_mmc->f_min = FH_SDHC_MIN_FREQ;
	fh_mshc->bind_mmc->f_max = FH_SDHCI_MAX_FREQ;
	fh_mshc->bind_mmc->f_init = FH_SDHCI_INIT_FREQ;	
	host->mmc->caps2 = (MMC_CAP2_NO_SDIO | MMC_CAP2_NO_SD);

	if((emmc_info->sup_mode & MSHC_MMC_MODE_HS400ES)){
		host->mmc->caps2 |= MMC_CAP2_HS400_ES | MMC_CAP2_HS400_1_8V;
	}else if(emmc_info->sup_mode & MSHC_MMC_MODE_HS400_1_8V){
		host->mmc->caps2 |= MMC_CAP2_HS400_1_8V;
	}else if(emmc_info->sup_mode & MSHC_MMC_MODE_HS200_1_8V)
		host->mmc->caps2 |= MMC_CAP2_HS200_1_8V_SDR;
	else{
		//only support sdr low speed..
		host->quirks2 = SDHCI_QUIRK2_BROKEN_HS200;
	}
	//pr_err("translate_auto_to_kernel host->mmc->caps2 is %x\n",host->mmc->caps2);
}

static int fh_mshc_probe(struct platform_device *pdev)
{
	struct sdhci_host *host;
	struct sdhci_pltfm_host *pltfm_host;
	struct mshc_lite_plat_info *mshc_plat_info;
	struct fh_mshc_lite *fh_mshc;
	int ret;
	u16 host_version;
	u32 phy_version;
	struct resource *regs;
	u32 reg;

	host = sdhci_pltfm_init(pdev, &sdhci_mc_pdata, sizeof(struct fh_mshc_lite));
	if (IS_ERR(host))
		return PTR_ERR(host);

	mshc_plat_info = (struct mshc_lite_plat_info *)pdev->dev.platform_data;
	if(!mshc_plat_info){
		pr_err("null mshc plat info...\n");
		BUG_ON(1);
	}
	pdev->dev.dma_mask = &pdev->dev.coherent_dma_mask;
	pltfm_host = sdhci_priv(host);
	fh_mshc = sdhci_pltfm_priv(pltfm_host);

	regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!regs) {
		pr_err("request resource error!\n");
		goto free_plat_mem;
	}

	host->ioaddr = ioremap_nocache(regs->start,
		regs->end - regs->start + 1);
	if (!host->ioaddr) {
		pr_err("no mem for fh mshc base!\n");
		ret = -ENOMEM;
		goto free_plat_mem;
	}
	fh_mshc->ctrl_regs = host->ioaddr;

	regs = platform_get_resource(pdev, IORESOURCE_MEM, 1);
	if (!regs) {
		pr_err("request resource error!\n");
		ret = -ENXIO;
		goto err_out_iounmap_host;
	}

	fh_mshc->phy_regs = ioremap_nocache(regs->start,
		regs->end - regs->start + 1);
	if (!fh_mshc->phy_regs) {
		pr_err("no mem for fh mshc phy base!\n");
		ret = -ENOMEM;
		goto err_out_iounmap_host;
	}

	fh_mshc->bind_mmc = host->mmc;
	fh_mshc->bind_host = host;
	fh_mshc->pdev = pdev;
	fh_mshc->mshc_plat_info = mshc_plat_info;

	if(mshc_plat_info->soc_clk_init)
		mshc_plat_info->soc_clk_init(host);
	if(mshc_plat_info->soc_ctrl_reset)
		mshc_plat_info->soc_ctrl_reset(host);
	fh_mshc_lite_hw_reset(fh_mshc);
	ret = bind_emmc_card_info(fh_mshc, mshc_plat_info);
	if(ret){
		pr_err("pls check emmc card..\n");
		goto err_out_iounmap_phy;
	}
	if(fh_mshc->bind_active_tuning_emmc->flag != FORCE_USR_SETTING){
		if(fh_mshc_auto_tuning_pata_get(fh_mshc) == AUTO_TUNING_PARA_ALREADY_GOT){
			pr_err("parse auto tuning para\n");
		}
		else{
			pr_err("auto tuning para failed....then backto low speed...\n");
		}
	}
	else{
		pr_err("using usr force setting...\n");
	}
	translate_auto_to_kernel(fh_mshc, fh_mshc->bind_active_tuning_emmc);
	if(mshc_plat_info->soc_clk_init)
		mshc_plat_info->soc_clk_init(host);
	if(mshc_plat_info->soc_ctrl_reset)
		mshc_plat_info->soc_ctrl_reset(host);
	fh_mshc_lite_hw_reset(fh_mshc);

	host->irq = platform_get_irq(pdev, 0);
	host->irq = irq_create_mapping(NULL, host->irq);
	if (host->irq < 0) {
		printk(KERN_ERR "no emmc host IRQ defined!\n");
		goto free_plat_mem;
	}
	mshc_phy_init(fh_mshc, INIT_INV_DISABLE);

	ret = sdhci_add_host(host);
	if (ret)
		goto free_plat_mem;

	return 0;


err_out_iounmap_phy:
	iounmap(fh_mshc->phy_regs);

err_out_iounmap_host:
	iounmap(fh_mshc->ctrl_regs);

free_plat_mem:
	sdhci_pltfm_free(pdev);
	return ret;


	return 0;

}

static int fh_mshc_remove(struct platform_device *pdev)
{

	pr_err("fh_mshc_remove get in....\n");
	struct sdhci_host *host = platform_get_drvdata(pdev);
	struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);

	int dead = (readl_relaxed(host->ioaddr + SDHCI_INT_STATUS) ==
		    0xffffffff);

	sdhci_remove_host(host, dead);
	sdhci_pltfm_free(pdev);

	return 0;
}

static const struct of_device_id fh_mshc_dt_match[] = {
	{.compatible = "fh,fh-mshc",},
	{},
};

MODULE_DEVICE_TABLE(of, fh_mshc_dt_match);

static struct platform_driver fh_mshc_driver = {
	.driver = {
		.name = "fh_mshc",
		.of_match_table = fh_mshc_dt_match,
	},
	.probe = fh_mshc_probe,
	.remove = fh_mshc_remove,
};

#ifdef CONFIG_DEFERRED_INITCALLS_EMMC
deferred_module_platform_driver(fh_mshc_driver);
#else
module_platform_driver(fh_mshc_driver);
#endif

__be32 mshc_in_aton(const char *str)
{
	unsigned long l;
	unsigned int val;
	int i;

	l = 0;
	for (i = 0; i < 4; i++)	{
		l <<= 8;
		if (*str != '\0') {
			val = 0;
			while (*str != '\0' && *str != '.' && *str != '\n') {
				val *= 10;
				val += *str - '0';
				str++;
			}
			l |= val;
			if (*str != '\0')
				str++;
		}
	}
	return htonl(l);
}

static int __init mshc_cmdline_parts_setup(char *s)
{
#if (0)
	char *cp, *raw;
	int num = 0;
	u32 phy_id = 0;
	u32 tmp_dl = 0;

	raw = s;
	while (raw && *raw) {
		if ((cp = strchr(raw, ':')))
			*cp++ = '\0';
		if (strlen(raw) > 0) {
			pr_err("dll parse: Parameter `%s'\n", raw);
			switch (num) {
			case 0:
			strcpy(g_name_array,raw);
			g_emmc_info.mmc_name = g_name_array;
			break;

			case 1:
			tmp_dl = mshc_in_aton(raw);
			//tmp_dl = 0 | rx data | rx cmd | tx data
			g_emmc_info.hs200_tx_data = ((tmp_dl) & 0xff);
			g_emmc_info.hs200_rx_cmd = ((tmp_dl >> 8) & 0xff);
			g_emmc_info.hs200_rx_data = ((tmp_dl >> 16) & 0xff);

			pr_err("hs200 :: tx_data : rx_cmd : rx_data = 0x%x : 0x%x : 0x%x\n",
			g_emmc_info.hs200_tx_data,
			g_emmc_info.hs200_rx_cmd,
			g_emmc_info.hs200_rx_data);

			break;

			case 2:
			tmp_dl = mshc_in_aton(raw);
			//tmp_dl = 0 | rx data | rx cmd | tx data
			g_emmc_info.hs400_tx_data = ((tmp_dl) & 0xff);
			g_emmc_info.hs400_rx_cmd = ((tmp_dl >> 8) & 0xff);
			g_emmc_info.hs400_rx_data = ((tmp_dl >> 16) & 0xff);

			pr_err("hs400 :: tx_data : rx_cmd : rx_data = 0x%x : 0x%x : 0x%x\n",
			g_emmc_info.hs400_tx_data,
			g_emmc_info.hs400_rx_cmd,
			g_emmc_info.hs400_rx_data);
			break;

			}
		}
		num++;
		raw = cp;
	}
#endif
	return 1;
}
__setup("mshc_dll=", mshc_cmdline_parts_setup);



MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZHANGY");
MODULE_DESCRIPTION("Fullhan MSHC driver");
